package com.hmdp.utils;


import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * 自定义的缓存工具类， 方便操作缓存
 */
@Slf4j
@Component
public class CatchClient {
   @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     *   将任意 java对象 序列化 为 json 并存储在　Redis 的 string类型的key中，并且设置　TTL时间
     * @param key      存于redis的key
     * @param value    存于 redis的 value
     * @param time     设置过期时间
     * @param unit     设置时间单位
     */
   public void set(String key, Object value, Long time, TimeUnit unit){
       stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
   }

    /**
     *  将java对象 带上一个 逻辑过期时间的 属性   序列化为json 存储在redis
     * 用于处理 缓存击穿 问题
     * @param key      存于redis的key
     * @param value    存于 redis的 value
     * @param time    设置过期时间
     * @param unit    设置时间单位
     */
   public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
       RedisData redisData = new RedisData();
       redisData.setData(value);
       redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
       stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
   }

    /**     根据指定 的 key 查询缓存，并反序列化 为指定类型，利用换粗空值的方法
     *      用于处理 缓存穿透问题
     *
     * @param keyPrefix     redis存放的key 的 业务前缀
     * @param id            数据库存的数据id
     * @param type          要返回的数据的数据类型
     * @param dbFallback    逻辑函数，由于在做数据库查询的时候 查询的表返回的值不固定，所以需要由外部调用者传入具体 逻辑
     * @param time          过期时间ttl
     * @param unit          时间单位
     * @return
     * @param <R>           泛型  泛指 返回值的 函数类型，只有在运行时才能确定
     * @param <ID>          业务id
     */
    public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback,Long time, TimeUnit unit){ //传入一个函数，ID表示参数 R表示返回值
        String key = keyPrefix+id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 查询 key是否存在
        if(StrUtil.isNotBlank(json)){
             //redis存在，直接返回
            return JSONUtil.toBean(json,type);
        }
        // 判断是否是空值，
        if(json !=null){
            return null;
        }
        //redis 不存在，根据id查数据库 , 此处对于不同的业务情况查询的表不同，返回值也不不确定， 这就需要函数式的方式， 由外部调用者决定里面的逻辑，Function<ID,R> dbFallback 就是
        R r  = dbFallback.apply(id);
        // 判断数据库 是否存在
        if(r ==null){
            //数据库不存在，将空值 写入 redis
            stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        // 数据库 存在， 写入redis
        this.set(key,r,time,unit);
        return r;
    }






    // 解决缓存击穿、 加锁！ 拿到锁的线程去拼装id，并加锁，其他等待
    private  boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);//如果直接返回 可能会做拆箱、拆箱过程中可能会空指针
    }
    //释放锁
    private  void unlock(String key){
        stringRedisTemplate.delete(key);
    }

    //逻辑过期时间、 枷锁、 开辟新的线程 去重塑 id 用到的线程池
    private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);



    public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id, Class<R> type,Function<ID,R> dbFallback,Long time, TimeUnit unit){
        String  key = keyPrefix+ id;
        log.debug("redis 获取 key的值");
        String json = stringRedisTemplate.opsForValue().get(key);
        if(StrUtil.isBlank(json)){
            log.debug("redis不存在key，直接返回shop");
            return null;
        }
        log.debug("redis存在key，判断是否过期");
        log.debug("获取redis存放的 RedisData的 expireTime");
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r  = JSONUtil.toBean((JSONObject) redisData.getData(),type);
        LocalDateTime expireTime = redisData.getExpireTime();
        log.debug("判断是否过期");
        if(expireTime.isAfter(LocalDateTime.now())){
            log.debug("未过期， 返回店铺信息");
            return r;
        }
        log.debug("过期，重建  缓存");
        log.debug("获取互斥锁");
        String lockKey = RedisConstants.LOCK_SHOP_KEY+id;
        boolean isLock = tryLock(lockKey);
        log.debug("判断是否 获取到锁");
        if( isLock ){
            try{
                log.debug("获取到锁, 开启独立线程 去做 缓存重建");
                CACHE_REBUILD_EXECUTOR.submit(()->{
                    log.debug("根据id 查询数据库");
                    R  r1 = dbFallback.apply(id);
                     log.debug("写入redis");
                     this.setWithLogicalExpire(key,r1,time,unit);
                });
            }catch (Exception e ){
                throw  new RuntimeException(e);
            }finally {
                unlock(lockKey);
            }
        }
        log.debug("没有获取到锁，直接将redis中过期的数据返回");
        return r;
    }




}
